﻿namespace Sai.Core
{
	using System;
	using System.Collections.Generic;
	using Sai.Core.Introspection;
	using Sai.Core.Rules;
	using System.Collections.ObjectModel;
    using System.Threading.Tasks;

	public class TypeIntrospectionBuilder
	{
		public TypeIntrospectionBuilder(IAssemblyIntrospector introspector, IWalkIntrospectedAssembliesFactory assemblyWalkerFactory, IWalkTypeDifferencesFactory typeWalkerFactory, IProductReleaseDifferenceFactory releaseDifferenceFactory, IAssemblyDifferenceFactory assemblyDifferenceFactory, IEnumerable<AssemblyComparison> comparisons, IProductReleaseProject project)
		{
			this.Introspector = introspector;
			this.TypeDifferenceFactory = typeWalkerFactory;
			this.AssemblyFactory = assemblyWalkerFactory;
			this.ComparisonList = new List<AssemblyComparison>(comparisons);
			this.ReleaseDifferenceFactory = releaseDifferenceFactory;
			this.AssemblyDifferenceFactory = assemblyDifferenceFactory;
            this.Project = project;
		}

		public event EventHandler<CancelBuildEventArgs> BuildStarting;

        public event EventHandler<CancelBuildEventArgs> PreBuildStarting;

		public event EventHandler<CancelBuildEventArgs> PreBuildComplete;

		public event EventHandler<FileProgessEventArgs> AnalysingAssembly;

        public event EventHandler<CancelBuildEventArgs> AnalysisComplete;

        public event EventHandler<CancelBuildEventArgs> PostBuildStarting;

        public event EventHandler<CancelBuildEventArgs> PostBuildComplete;

		public event EventHandler<BuildErrorEventArgs> BuildError;

		public event EventHandler BuildComplete;

		private List<AssemblyComparison> ComparisonList { get; set; }

		private IAssemblyIntrospector Introspector { get; set; }

		private IWalkTypeDifferencesFactory TypeDifferenceFactory { get; set; }

		private IWalkIntrospectedAssembliesFactory AssemblyFactory { get; set; }

		private IProductReleaseDifferenceFactory ReleaseDifferenceFactory { get; set; }

		private IAssemblyDifferenceFactory AssemblyDifferenceFactory { get; set; }

		public IProductReleaseDifference ProductReleaseDifference { get; private set; }

		private List<IAssemblyDifference> assemblyDifferences = new List<IAssemblyDifference>();

        private IProductReleaseProject Project { get; set; }

		public ReadOnlyCollection<IAssemblyDifference> AssemblyDifferences 
		{ 
			get
			{
				return new ReadOnlyCollection<IAssemblyDifference>(this.assemblyDifferences);
			}
		}

        public void Build(IReportProgress progress)
		{
            try
            {
                this.OnBuildStarting();

                this.ExecutePreBuildSteps();

                this.BuildInternal(progress);

                this.ExecutePostBuildSteps();

                this.OnBuildComplete();
            }
            catch (BuildCancelledException)
            {
                throw;
            }
            catch (Exception ex)
            {
                this.OnError(ex);
            }
		}

		private void ExecutePreBuildSteps()
		{
			this.OnPreBuildStarting();
			this.CheckPreconditions();
			this.OnPreBuildComplete();
		}

		private void CheckPreconditions()
		{
			/* nothing */
		}

        private void BuildInternal(IReportProgress progress)
		{
			int total = this.ComparisonList.Count;
			int index = 0;

			IProductReleaseDifference releaseDifference = this.ReleaseDifferenceFactory.Create();

            ChurnCalculator overallChurn = new ChurnCalculator();
                 
			// return updated list - modify indeterminate elements to changed or no change
			foreach (var candidate in this.ComparisonList)
			{
				if (candidate.Result == AssemblyComparisonResult.Added && this.Project.ReportOnAdditions)
				{
					releaseDifference.Added(candidate.New);
				}
                else if (candidate.Result == AssemblyComparisonResult.Removed && this.Project.ReportOnRemovals)
				{
					releaseDifference.Removed(candidate.Old);
				}

				// necessary - can we strip out all but the indeterminate ones...
				if (candidate.Result == AssemblyComparisonResult.Indeterminate
                    && (this.Project.ReportOnChanges
                    || this.Project.ReportOnObsolete))
				{
					this.OnAnalysingAssembly(candidate.Old.Name, ++index, total);

					var oldAssembly = this.Introspector.IntrospectAssembly(candidate.Old);
					var newAssembly = this.Introspector.IntrospectAssembly(candidate.New);

					IWalkTypeDifferences typeDifferenceWalker = this.TypeDifferenceFactory.Create();
					IWalkIntrospectedAssemblies assemblyWalker = this.AssemblyFactory.Create();
					IAssemblyDifference assemblyDifference = this.AssemblyDifferenceFactory.Create(oldAssembly, newAssembly);

					assemblyWalker.Walk(oldAssembly, newAssembly, typeDifferenceWalker, assemblyDifference, this.Project);

                    if (assemblyDifference.HasAnyChanges)
                    {
                        ChurnCalculator assemblyChurn = new ChurnCalculator(oldAssembly, newAssembly, assemblyDifference);
                        assemblyDifference.PercentChurn = assemblyChurn.Calculate();

                        overallChurn.Increment(assemblyChurn);

                        this.assemblyDifferences.Add(assemblyDifference);
                        releaseDifference.Changed(candidate.New);
                    }
                    else
                    {
                        overallChurn.Increment(new ChurnCalculator(oldAssembly, newAssembly));
                    }

					// now compare the two...
					this.OnAnalysisComplete();
				}
			}

            releaseDifference.PercentChurn = overallChurn.Calculate();
            this.ProductReleaseDifference = releaseDifference;
		}
             

		private void ExecutePostBuildSteps()
		{
			this.OnPostBuildStarting();

			/* nothing */

			this.OnPostBuildComplete();
		}

		private void OnBuildStarting()
		{
            CancelBuildEventArgs cancel = new CancelBuildEventArgs();

            this.BuildStarting.Fire(this, cancel);
        
            if (cancel.CancelAction)
                throw new BuildCancelledException();
        }

		private void OnPreBuildStarting()
		{
            CancelBuildEventArgs cancel = new CancelBuildEventArgs();

            this.PreBuildStarting.Fire(this, cancel);
        
            if (cancel.CancelAction)
                throw new BuildCancelledException();
        }

		private void OnPreBuildComplete()
		{
            CancelBuildEventArgs cancel = new CancelBuildEventArgs();

            this.PreBuildComplete.Fire(this, cancel);
        
            if (cancel.CancelAction)
                throw new BuildCancelledException();
        }

		private void OnAnalysingAssembly(string file, int item, int itemTotal)
		{
            FileProgessEventArgs progress = new FileProgessEventArgs(file, item, itemTotal);

            this.AnalysingAssembly.Fire(this, progress);

            if (progress.CancelAction)
                throw new BuildCancelledException();
        }

		private void OnAnalysisComplete()
		{
            CancelBuildEventArgs cancel = new CancelBuildEventArgs();

            this.AnalysisComplete.Fire(this, cancel);
        
            if (cancel.CancelAction)
                throw new BuildCancelledException();
        }

		private void OnPostBuildStarting()
		{
            CancelBuildEventArgs cancel = new CancelBuildEventArgs();

            this.PostBuildStarting.Fire(this, cancel);

            if (cancel.CancelAction)
                throw new BuildCancelledException();
		}

		private void OnPostBuildComplete()
		{
            CancelBuildEventArgs cancel = new CancelBuildEventArgs();

            this.PostBuildComplete.Fire(this, cancel);

            if (cancel.CancelAction)
                throw new BuildCancelledException();
        }

		private void OnError(Exception ex)
		{
			this.BuildError.Fire(this, new BuildErrorEventArgs(ex));
		}

		private void OnBuildComplete()
		{
			this.BuildComplete.Fire(this);
		}
	}
}
